/**
 * Copyright (c) 2007, Blue Hole Software. All rights reserved.
 * Code licensed under the Apache 2.0 License:
 * http://www.apache.org/licenses/
 */
package org.bhf.json;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.logging.Level;

/**
 * Creates reader classes for parsing JSON into a particular type.
 *
 * A reader has a single method, read( T object, JSONTokenizer tokens, JSONParser reader ),
 * that returns the particular type (T) object.
 */
// TODO Carefully extract common docde from arrays and lists
class JSONParserGenerator
{
    /** Type to parser class name */
    private Set<String> typesInProgress = new HashSet<String>();

    /**
     * Resolve a parser class into a method for the given type.
     * @param type Type of object to parse
     * @param parserClassName Name of parsers
     * @return The resolved method
     */
    Method  resolveParser( final Type type, final String parserClassName )
    {
        try
        {
            final Class<?>  parserClass = Class.forName( parserClassName, true, Thread.currentThread().getContextClassLoader() );
            final Class     clazz       = Util.digRawType( type );
            return parserClass.getMethod( "parse", clazz, JSONTokenizer.class, JSONParser.class );
        }
        catch( ClassNotFoundException cnfe )
        {
            Package.log.log( Level.SEVERE, "Unable to load created parser class " + parserClassName );
            throw new RuntimeException( cnfe );
        }
        catch( NoSuchMethodException e )
        {
            Package.log.log( Level.SEVERE, "Missing parse method on parser " + parserClassName );
            throw new RuntimeException( e );
        }
        catch( UnsupportedTypeException e )
        {
            Package.log.log( Level.SEVERE, "Unsupported type " + type.toString() );
            throw new RuntimeException( e );
        }
    }

    /**
     * Get a parser name for the given type. The result may be a built-in or generated parser, as needed. If
     * the parser needs to be generated, it will be loaded prior to return.
     *
     * @param type The type of object to be read
     * @return Class name of the parser. The method name is always parse, with a signature appropriate to the type.
     * @throws java.io.IOException Write error
     */
    String  getParser( final Type type )
        throws IOException
    {
        // Try to get it from the class loader first
        String parserClassName = Util.getReaderClassNameForType( type );

        try
        {
            Class.forName( parserClassName, true, Thread.currentThread().getContextClassLoader() );
            return parserClassName;
        }
        catch( ClassNotFoundException cnfe )
        {
            if( NativeJSONParsers.hasBuiltInSupport( type ) )
            {
                return NativeJSONParsers.class.getName();
            }
            else
            {
                return generateFor( type );
            }
        }
    }

    //
    // Private
    //

    /**
     * Generate a parser for the given type.
     *
     * @param type The type of object to be read
     * @return Name of class that will implement this parser
     * @throws java.io.IOException Write error
     */
    private String  generateFor( final Type type )
        throws IOException
    {
        final String parserClassName = Util.getReaderClassNameForType( type );

        // If we're already working on the parser, just return the name for forward reference
        if( typesInProgress.contains( parserClassName ) )
        {
            return parserClassName;
        }
        else
        {
            typesInProgress.add( parserClassName );
        }

        if( type instanceof Class )
        {
            final Class clazz = Class.class.cast( type );

            if( Enum.class.isAssignableFrom( clazz ) )
            {
                return generateBodyForEnum( type ).getName();
            }
            else if( clazz.isArray() )
            {
                return generateBodyForArray( clazz ).getName();
            }
            else if( List.class.isAssignableFrom( clazz ) )
            {
                return generateBodyForList( clazz ).getName();
            }
            else if( Map.class.isAssignableFrom( clazz ) )
            {
                return generateBodyForMap( clazz ).getName();
            }
            else
            {
                return generateBodyForPOJO( clazz ).getName();
            }
        }
        else if( type instanceof GenericArrayType )
        {
            return generateBodyForArray( GenericArrayType.class.cast( type ) ).getName();
        }
        else if( type instanceof ParameterizedType )
        {
            final ParameterizedType pType = ParameterizedType.class.cast( type );
            final Type rawType = pType.getRawType();

            if( rawType instanceof Class && Map.class.isAssignableFrom( (Class)rawType ) )
            {
                return generateBodyForMap( pType ).getName();
            }
            else if( rawType instanceof Class && List.class.isAssignableFrom( (Class)rawType ) )
            {
                return generateBodyForList( type ).getName();
            }
            else if( rawType instanceof Class )
            {
                return generateBodyForPOJO( Class.class.cast( rawType ) ).getName();
            }
            else
            {
                throw new UnsupportedTypeException( type );
            }
        }
        else
        {
            throw new UnsupportedTypeException( type );
        }
    }

    //
    // Map
    //

    private Class<?> generateBodyForMap( final Type type )
        throws IOException
    {
        final StringBuffer body = new StringBuffer();
        final Type[] types      = Util.getMapComponentTypes( type );
        final Type keyType      = types[0];
        final String keyCast    = Util.getDeclarationName( keyType, true );
        final Type valueType    = types[1];
        final String valueCast  = Util.getDeclarationName( valueType, true );

        body.append( "char c=tokens.nextClean();" );
        body.append( "if(c!='{'){");
            body.append( "tokens.back();" );
            body.append( "if(\"null\".equals(tokens.nextUnQuotedString()))return null;" );
            body.append( "throw new org.bhf.json.SyntaxError(\"Bad object syntax. Expected { \",tokens);");
        body.append( "}" );

        body.append( "if(object==null){");
        body.append( "object=new java.util.HashMap();" );
        body.append( "}");

        body.append( "c=tokens.nextClean();" );
        body.append( "if(c!='}'){");
            body.append( "tokens.back();" );

            body.append( "while(true) {");
                body.append( keyCast ).append( " key=");
                    generateValueParserFragment( keyType, "(" + keyCast + ")null", body );
                    body.append( ";" );

                body.append( "if(tokens.nextClean()!=':')throw new org.bhf.json.SyntaxError(\"Bad object syntax. Expected :\",tokens);");
                body.append( valueCast ).append( " value=(" ).append( valueCast ).append( ")object.get(key);" );

                body.append( "reader.getContext().add(key);" );
                body.append( "object.put(key," );
                    generateValueParserFragment( valueType, "value", body );
                body.append( ");" );
                body.append( "reader.getContext().remove(reader.getContext().size()-1);" );

                body.append( "c=tokens.nextClean();");
                body.append( "switch(c){");
                    body.append( "case '}':" );
                        body.append( "return object;" );
                    body.append( "case ',':" );
                        body.append( "break;" );
                    body.append( "default:" );
                        body.append( "throw new org.bhf.json.SyntaxError(\"Bad object syntax. Expected } or , \",tokens);");
                body.append( "}" ); // End while(true)
            body.append( "}}else return object;");

        return generateReaderFromMethodBody( type, body.toString() );
    }

    //
    // POJO
    //

    private Class<?> generateBodyForPOJO( final Class type )
        throws IOException
    {
        final StringBuffer body = new StringBuffer();
        final Map<Integer,List<Util.PropertyInfo>> propertiesByHash = new HashMap<Integer,List<Util.PropertyInfo>>();

        // Sort by hash in preparation for a switch on hash
        for( final Util.PropertyInfo propertyInfo : Util.getPropertyInfo( type ) )
        {
            if( propertyInfo.getGetter() != null && propertyInfo.getSetter() != null )
            {
                final Integer hash = propertyInfo.getProperty().hashCode();

                List<Util.PropertyInfo> properties = propertiesByHash.get( hash );
                if( properties == null )
                {
                    properties = new ArrayList<Util.PropertyInfo>(2);
                    propertiesByHash.put( hash, properties );
                }
                properties.add( propertyInfo );
            }
        }

        body.append( "char c=tokens.nextClean();" );
        body.append( "if(c!='{'){");
            body.append( "tokens.back();" );
            body.append( "if(\"null\".equals(tokens.nextUnQuotedString()))return null;" );
            body.append( "throw new org.bhf.json.SyntaxError(\"Bad object syntax\",tokens);");
        body.append( "}" );
        if( canConstruct( type ) && mayBeMutable( type ) )
        {
            body.append( "if(object==null){");
            body.append( "object=new ").append( Util.getDeclarationName( type, true ) ).append( "();" );
            body.append( "}");
        }
        body.append( "if(object==null)throw new org.bhf.json.SyntaxError(\"No default ctor for class " ).append( Util.getDeclarationName( type, true ) ).append( "\",tokens);" );

        body.append( "c=tokens.nextClean();" );
        body.append( "if(c!='}'){");
            body.append( "tokens.back();" );
            body.append( "while(true) {");
                body.append( "String property=tokens.nextAnyValueString();if(property==null)property=\"null\";");
                body.append( "if(tokens.nextClean()!=':')throw new org.bhf.json.SyntaxError(\"Bad object syntax. Expected :\",tokens);");
                if( propertiesByHash.size() > 0 )
                {
                    body.append( "reader.getContext().add(property);" );
                    body.append( "switch(property.hashCode()){" );

                        for( final Map.Entry<Integer,List<Util.PropertyInfo>> entry : propertiesByHash.entrySet() )
                        {
                            final Integer hash = entry.getKey();
                            final boolean collision = entry.getValue().size() > 1;

                            body.append( "case " ).append( hash ).append( ':' );
                            for( final Util.PropertyInfo propertyInfo : entry.getValue() )
                            {
                                final Type propertyType = propertyInfo.getType();
                                final String propertyDecl = Util.getDeclarationName( propertyType, true );

                                if( collision )
                                    body.append( "if(\"" ).append( propertyInfo.getProperty() ).append( "\".equals(property))" );
                                body.append( '{' );
                                body.append( propertyDecl ).append( " value=" ).append( getDefaultInitializer( propertyType ) ).append( ';' );
                                if( mayBeMutable( propertyType ) && propertyInfo.getGetter() != null )
                                {
                                    body.append( "value=object." ).append( propertyInfo.getGetter().getName() ).append( "();" );
                                }
                                body.append( "object." ).append( propertyInfo.getSetter().getName() ).append( "(" );
                                generateValueParserFragment( propertyType, "value", body );
                                body.append( ");" );
                                body.append( "}" );
                            }
                            body.append( "break;" );
                        }
                        body.append( "default:" );
                            body.append( "{");
                            body.append( " org.bhf.json.ConversionError error = new org.bhf.json.ConversionError(reader.getContext(),tokens.nextAnyValueString(),null);" );
                            body.append( "reader.getMessages().add(error);" );
                            body.append( "}" );
                            body.append( "break;" );

                    body.append( "}" );
                    body.append( "reader.getContext().remove(reader.getContext().size()-1);" );
                }
                body.append( "c=tokens.nextClean();");
                body.append( "switch(c){");
                    body.append( "case '}':" );
                        body.append( "return object;" );
                    body.append( "case ',':" );
                        body.append( "break;" );
                    body.append( "default:" );
                        body.append( "throw new org.bhf.json.SyntaxError(\"Bad object syntax. Expected } or , and found \" + c,tokens);");
                body.append( "}" );
            body.append( "}}else return object;");

        return generateReaderFromMethodBody( type, body.toString() );
    }

    private boolean canConstruct( final Type type )
    {
        try
        {
            final Class clazz = Util.digRawType( type );
            for( final Constructor ctor : clazz.getConstructors() )
            {
                if( ctor.getParameterTypes().length == 0 && Modifier.isPublic( ctor.getModifiers() ) )
                {
                    return true;
                }
            }
            return false;
        }
        catch( final UnsupportedTypeException e )
        {
            return false;
        }
    }

    private String  getDefaultInitializer( final Type type )
    {
        if( type instanceof Class )
        {
            final Class clazz = Class.class.cast( type );
            if( Boolean.TYPE.equals( clazz ) )          return "false";
            if( Byte.TYPE.equals( clazz ) )             return "(byte)0";
            if( Short.TYPE.equals( clazz ) )            return "(short)0";
            if( Integer.TYPE.equals( clazz ) )          return "0";
            if( Long.TYPE.equals( clazz ) )             return "0L";
            if( Float.TYPE.equals( clazz ) )            return "0.0F";
            if( Double.TYPE.equals( clazz ) )           return "0.0";
        }
        return "null";
    }

    private boolean mayBeMutable( final Type type )
    {
        if( type instanceof Class )
        {
            final Class clazz = Class.class.cast( type );
            if( clazz.isPrimitive() )                   return false;
            if( Boolean.class.equals( clazz ) )         return false;
            if( Byte.class.equals( clazz ) )            return false;
            if( Short.class.equals( clazz ) )           return false;
            if( Integer.class.equals( clazz ) )         return false;
            if( Long.class.equals( clazz ) )            return false;
            if( Float.class.equals( clazz ) )           return false;
            if( Double.class.equals( clazz ) )          return false;
            if( String.class.equals( clazz ) )          return false;
            if( Enum.class.isAssignableFrom( clazz ) )  return false;
        }
        return true;
    }

    //
    // Arrays
    //

    private Class<?> generateBodyForArray( final GenericArrayType type )
        throws IOException
    {
        final StringBuffer body = new StringBuffer();
        final Type componentType = type.getGenericComponentType();
        final String componentCast = Util.getDeclarationName( componentType, true );
        final String arrayCast = Util.getDeclarationName( type, true );

        // Accumulate the elements in a local list. Copy at the end
        body.append( componentCast ).append( "[] accumulator=new " ).append( componentCast ).append( "[1024];" );

        body.append( "char c=tokens.nextClean();" );
        body.append( "if(c!='['){");
            body.append( "tokens.back();" );
            body.append( "if(\"null\".equals(tokens.nextUnQuotedString()))return null;" );
            body.append( "throw new org.bhf.json.SyntaxError(\"Bad array syntax\",tokens);");
        body.append( "}" );

        // We will have already consumed the leading '['
        body.append( "c=tokens.nextClean();if(c!=']'){tokens.back();" );
        body.append( "for( int i = 0; true; i++ ) {");
            body.append( "if( i >= accumulator.length ){");
                body.append( componentCast ).append( "[] save = accumulator;" );
                body.append( "accumulator=new " ).append( componentCast ).append( "[i+1024];" );
                body.append( "System.arraycopy(save,0,accumulator,0,save.length);" );
            body.append( "}");

            body.append( componentCast ).append( " accessor;");
            body.append( "if(object!=null&&i<object.length){").append( "accessor=object[i];}" );
            body.append( "else{accessor=accumulator[i];}");
            body.append( "reader.getContext().add(Integer.toString(i));" );
            body.append( "accumulator[i]=" );
                generateValueParserFragment( componentType, "accessor", body );
            body.append( ";" );
            body.append( "reader.getContext().remove(reader.getContext().size()-1);" );

            body.append( "c=tokens.nextClean();" ); // nested case breaks compiler

            body.append( "if(c==','){");
                body.append( "continue;");
            body.append( "}");

            body.append( "if(c==']'){" );
                body.append( "if(object==null||object.length!=i+1){" );
                    body.append( "object=new " ).append( componentCast ).append( "[i+1];" );
                body.append( "}" );
                body.append( "System.arraycopy(accumulator,0,object,0,i+1);" );
                body.append( "return object;" );
            body.append( "}" );

            body.append( "throw new org.bhf.json.SyntaxError(\"Bad array syntax\",tokens);");

        body.append( "}}else return new " ).append( componentCast ).append( "[0];");

        body.append( "return(" ).append( arrayCast ).append( ")null;" ); // Dummy return for compiler bug

        return generateReaderFromMethodBody( type, body.toString() );
    }

    private Class<?> generateBodyForArray( final Class type )
        throws IOException
    {
        final StringBuffer body = new StringBuffer();
        final Class componentType = type.getComponentType();
        final String componentCast = Util.getDeclarationName( componentType, true );
        final String arrayCast = Util.getDeclarationName( type, true );

        // Accumulate the elements in a local list. Copy at the end
        body.append( arrayCast ).append( " accumulator=new " ).append( componentCast ).append( "[1024];" );

        body.append( "char c=tokens.nextClean();" );
        body.append( "if(c!='['){");
            body.append( "tokens.back();" );
            body.append( "if(\"null\".equals(tokens.nextUnQuotedString()))return null;" );
            body.append( "throw new org.bhf.json.SyntaxError(\"Bad array syntax\",tokens);");
        body.append( "}" );

        // We will have already consumed the leading '['
        body.append( "c=tokens.nextClean();if(c!=']'){tokens.back();" );
        body.append( "for( int i = 0; true; i++ ) {");
            body.append( "if( i >= accumulator.length ){");
                body.append( arrayCast ).append( " save = accumulator;" );
                body.append( "accumulator=new " ).append( componentCast ).append( "[i+1024];" );
                body.append( "System.arraycopy(save,0,accumulator,0,save.length);" );
            body.append( "}");

            body.append( componentCast ).append( " accessor;");
            body.append( "if(object!=null&&i<object.length){").append( "accessor=object[i];}" );
            body.append( "else{accessor=accumulator[i];}");
            body.append( "reader.getContext().add(Integer.toString(i));" );
            body.append( "accumulator[i]=" );
                generateValueParserFragment( componentType, "accessor", body );
            body.append( ";" );
            body.append( "reader.getContext().remove(reader.getContext().size()-1);" );

            body.append( "c=tokens.nextClean();" ); // nested case breaks compiler

            body.append( "if(c==','){");
                body.append( "continue;");
            body.append( "}");

            body.append( "if(c==']'){" );
                body.append( "if(object==null||object.length!=i+1){" );
                    body.append( "object=new " ).append( componentCast ).append( "[i+1];" );
                body.append( "}" );
                body.append( "System.arraycopy(accumulator,0,object,0,i+1);" );
                body.append( "return object;" );
            body.append( "}" );

            body.append( "throw new org.bhf.json.SyntaxError(\"Bad array syntax\",tokens);");

        body.append( "}}else return new " ).append( componentCast ).append( "[0];");
        body.append( "return(" ).append( arrayCast ).append( ")null;" ); // Dummy return for compiler bug

        return generateReaderFromMethodBody( type, body.toString() );
    }

    //
    // Collections
    //
    
    private Class<?> generateBodyForList( final Type type )
        throws IOException
    {
        final StringBuffer body = new StringBuffer();
        final Type componentType = Util.getCollectionComponentType( type );
        final String componentCast = Util.getDeclarationName( componentType, true );
        final String arrayCast = Util.getDeclarationName( type, true );

        // Accumulate the elements in a local list. Copy at the end
        body.append( componentCast ).append( "[] accumulator=new " ).append( componentCast ).append( "[1024];" );

        body.append( "char c=tokens.nextClean();" );
        body.append( "if(c!='['){");
            body.append( "tokens.back();" );
            body.append( "if(\"null\".equals(tokens.nextUnQuotedString()))return null;" );
            body.append( "throw new org.bhf.json.SyntaxError(\"Bad array syntax\",tokens);");
        body.append( "}" );

        // We will have already consumed the leading '['
        body.append( "c=tokens.nextClean();if(c!=']'){tokens.back();" );
        body.append( "for( int i = 0; true; i++ ) {");
            body.append( "if( i >= accumulator.length ){");
                body.append( componentCast ).append( "[] save = accumulator;" );
                body.append( "accumulator=new " ).append( componentCast ).append( "[i+1024];" );
                body.append( "System.arraycopy(save,0,accumulator,0,save.length);" );
            body.append( "}");

            body.append( componentCast ).append( " accessor;");
            body.append( "if(object!=null&&i<object.size()){" ).append( "accessor=(" ).append( componentCast ).append( ")object.get(i);}" );
            body.append( "else{accessor=accumulator[i];}");
            body.append( "reader.getContext().add(Integer.toString(i));" );
            body.append( "accumulator[i]=(" ).append( componentCast ).append( ")" );
                generateValueParserFragment( componentType, "accessor", body );
            body.append( ";" );
            body.append( "reader.getContext().remove(reader.getContext().size()-1);" );

            body.append( "c=tokens.nextClean();" ); // nested case breaks compiler

            body.append( "if(c==','){");
                body.append( "continue;");
            body.append( "}");

            body.append( "if(c==']'){" );
                body.append( "if(object==null){" );
                    body.append( "object=new java.util.ArrayList(i+1);" );
                body.append( "}" );
                body.append( "for(int j=object.size()-1;j>i;j--)object.remove(j);" );
                body.append( "for(int j=object.size();j<=i;j++)object.add(j,accumulator[j]);" );
                body.append( "return object;" );
            body.append( "}" );

            body.append( "throw new org.bhf.json.SyntaxError(\"Bad array syntax\",tokens);");

        body.append( "}}else return new java.util.ArrayList(0);" );
        body.append( "return(" ).append( arrayCast ).append( ")null;" ); // Dummy return for compiler bug

        return generateReaderFromMethodBody( type, body.toString() );
    }

    //
    // Maps
    //


    //
    // Enums
    //

    private Class<?>    generateBodyForEnum( final Type type )
        throws IOException
    {
        final StringBuffer body = new StringBuffer();

        body.append( "String value = tokens.nextAnyValueString();" );
        body.append( "if(value==null) return null;" );
        body.append( "try{" );
        body.append( "return " ).append( '(' ).append( Util.getDeclarationName( type, true ) ).append( ')' ).
            append( "Enum.valueOf(" ).
                append( Util.getDeclarationName( type, true ) ).append( ".class,value);" );
        body.append( "}catch(IllegalArgumentException iae){" );
        body.append( " org.bhf.json.ConversionError error = new org.bhf.json.ConversionError(reader.getContext(),value," ).append( Util.getDeclarationName( type, true ) ).append(".class").append( ");" );
        body.append( "reader.getMessages().add(error);" );
        body.append( "return object;" );
        body.append( "}" );

        return generateReaderFromMethodBody( type, body.toString() );
    }

    //
    // Common
    //

    /**
     * Generate a code fragment to parse the given type. This fragment will be a call to either
     * a generated or built-in parse. In either case, the return result will be of type' raw
     * type: no cast required.
     *
     * @param type Type returned by the fragment
     * @param accessor Snippit to access incoming object
     * @param body Code accumulator
     * @throws IOException Write error
     */
    private void    generateValueParserFragment( final Type type, final String accessor, final StringBuffer body )
        throws IOException
    {
        final String parserClass = getParser( type ); // Ensures that we are also generating the inner parser as well

        body.append( parserClass );
        body.append( ".parse(" ).append( accessor ).append( ",tokens,reader)" );
    }

    /**
     * Generate a reader, given the body of the read method.
     *
     * @param type The type of object to be returned from this reader
     * @param methodBody Will be modified, do not reuse.
     * @return Class implementing the writer for this type
     * @throws IOException Write error
     */
    private Class<?> generateReaderFromMethodBody( final Type type, final String methodBody )
        throws IOException
    {
        final String parserClassName = Util.getReaderClassNameForType( type );

        if( Package.log.isLoggable( Level.INFO ) )
        {
            Package.log.log(
                Level.INFO,
                "Generating JSON parser " + parserClassName +
                " for " + Util.getDeclarationName( type, true )
            );
        }

        final ClassPool pool = Util.getClassPool();

        // Open class
        final CtClass reader;
        reader = pool.makeClass( parserClassName );

        // Open method
        final StringBuffer body = new StringBuffer();

        body.append( "public static " ).
            append( Util.getDeclarationName( type, true ) ).
            append( " parse(" ).
                append( Util.getDeclarationName( type, true ) ).append( " object" ).append( ',' ).
                append( " org.bhf.json.JSONTokenizer tokens" ).append( ',' ).
                append( " org.bhf.json.JSONParser reader" ).
            append( ") throws java.io.IOException {" );
        body.append( methodBody );
        body.append( "}" );
        try
        {
//            System.out.println( body.toString() );
            // Close method
            {
                final CtMethod write = CtNewMethod.make( body.toString(), reader );
                reader.addMethod( write );
            }
            // Close class
            return reader.toClass();
        }
        catch( CannotCompileException e )
        {
            throw new RuntimeException( e ); // Soften because a compile error implies a problem with our generated code
        }
    }
}
